/*
 * XMLUtil.java NanoXML/Java $Revision: 1.5 $ $Date: 2002/02/03 21:19:38 $
 * $Name: RELEASE_2_2_1 $ This file is part of NanoXML 2 for Java. Copyright (C)
 * 2000-2002 Marc De Scheemaecker, All Rights Reserved. This software is
 * provided 'as-is', without any express or implied warranty. In no event will
 * the authors be held liable for any damages arising from the use of this
 * software. Permission is granted to anyone to use this software for any
 * purpose, including commercial applications, and to alter it and redistribute
 * it freely, subject to the following restrictions: 1. The origin of this
 * software must not be misrepresented; you must not claim that you wrote the
 * original software. If you use this software in a product, an acknowledgment
 * in the product documentation would be appreciated but is not required. 2.
 * Altered source versions must be plainly marked as such, and must not be
 * misrepresented as being the original software. 3. This notice may not be
 * removed or altered from any source distribution.
 */
package org.freeplane.n3.nanoxml;

import java.io.IOException;
import java.io.Reader;

/**
 * Utility methods for NanoXML.
 * 
 * @author Marc De Scheemaecker
 * @version $Name: RELEASE_2_2_1 $, $Revision: 1.5 $
 */
class XMLUtil {
	/**
	 * Returns true if the data starts with <I>literal</I>. Enough chars are
	 * read to determine this result.
	 * 
	 * @param reader
	 *            the reader
	 * @param literal
	 *            the literal to check
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static boolean checkLiteral(final IXMLReader reader, final String literal) throws IOException, XMLParseException {
		for (int i = 0; i < literal.length(); i++) {
			if (reader.read() != literal.charAt(i)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Throws an XMLParseException to indicate that extra data is encountered in
	 * a closing tag.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 */
	static void errorClosingTagNotEmpty(final String systemID, final int lineNr) throws XMLParseException {
		throw new XMLParseException(systemID, lineNr, "Closing tag must be empty");
	}

	/**
	 * Throws an XMLParseException to indicate that an expected string is not
	 * encountered.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param expectedString
	 *            the string that is expected
	 */
	static void errorExpectedInput(final String systemID, final int lineNr, final String expectedString)
	        throws XMLParseException {
		throw new XMLParseException(systemID, lineNr, "Expected: " + expectedString);
	}

	/**
	 * Throws an XMLValidationException to indicate that an attribute has an
	 * invalid value.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param elementName
	 *            the name of the element
	 * @param attributeName
	 *            the name of the attribute
	 * @param attributeValue
	 *            the value of that attribute
	 */
	static void errorInvalidAttributeValue(final String systemID, final int lineNr, final String elementName,
	                                       final String attributeName, final String attributeValue)
	        throws XMLValidationException {
		throw new XMLValidationException(XMLValidationException.ATTRIBUTE_WITH_INVALID_VALUE, systemID, lineNr,
		    elementName, attributeName, attributeValue, "Invalid value for attribute " + attributeName);
	}

	/**
	 * Throws an XMLParseException to indicate that an entity could not be
	 * resolved.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param entity
	 *            the name of the entity
	 */
	static void errorInvalidEntity(final String systemID, final int lineNr, final String entity)
	        throws XMLParseException {
		throw new XMLParseException(systemID, lineNr, "Invalid entity: `&" + entity + ";'");
	}

	/**
	 * Throws an XMLParseException to indicate that a string is not expected at
	 * this point.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param unexpectedString
	 *            the string that is unexpected
	 */
	static void errorInvalidInput(final String systemID, final int lineNr, final String unexpectedString)
	        throws XMLParseException {
		throw new XMLParseException(systemID, lineNr, "Invalid input: " + unexpectedString);
	}

	/**
	 * Throws an XMLValidationException to indicate that an attribute is
	 * missing.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param elementName
	 *            the name of the element
	 * @param attributeName
	 *            the name of the missing attribute
	 */
	static void errorMissingAttribute(final String systemID, final int lineNr, final String elementName,
	                                  final String attributeName) throws XMLValidationException {
		throw new XMLValidationException(XMLValidationException.MISSING_ATTRIBUTE, systemID, lineNr, elementName,
		    attributeName,
		    /* attributeValue */null, "Element " + elementName + " expects an attribute named " + attributeName);
	}

	/**
	 * Throws an XMLValidationException to indicate that an element is missing.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param parentElementName
	 *            the name of the parent element
	 * @param missingElementName
	 *            the name of the missing element
	 */
	static void errorMissingElement(final String systemID, final int lineNr, final String parentElementName,
	                                final String missingElementName) throws XMLValidationException {
		throw new XMLValidationException(XMLValidationException.MISSING_ELEMENT, systemID, lineNr, missingElementName,
		/* attributeName */null,
		/* attributeValue */null, "Element " + parentElementName + " expects to have a " + missingElementName);
	}

	/**
	 * Throws an XMLValidationException to indicate that a #PCDATA element was
	 * missing.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param parentElementName
	 *            the name of the parent element
	 */
	static void errorMissingPCData(final String systemID, final int lineNr, final String parentElementName)
	        throws XMLValidationException {
		throw new XMLValidationException(XMLValidationException.MISSING_PCDATA, systemID, lineNr,
		/* elementName */null,
		/* attributeName */null,
		/* attributeValue */null, "Missing #PCDATA in element " + parentElementName);
	}

	/**
	 * Throws an XMLValidationException to indicate that an attribute is
	 * unexpected.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param elementName
	 *            the name of the element
	 * @param attributeName
	 *            the name of the unexpected attribute
	 */
	static void errorUnexpectedAttribute(final String systemID, final int lineNr, final String elementName,
	                                     final String attributeName) throws XMLValidationException {
		throw new XMLValidationException(XMLValidationException.UNEXPECTED_ATTRIBUTE, systemID, lineNr, elementName,
		    attributeName,
		    /* attributeValue */null, "Element " + elementName + " did not expect an attribute " + "named "
		            + attributeName);
	}

	/**
	 * Throws an XMLParseException to indicate that a CDATA section is
	 * unexpected at this point.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 */
	static void errorUnexpectedCDATA(final String systemID, final int lineNr) throws XMLParseException {
		throw new XMLParseException(systemID, lineNr, "No CDATA section is expected here");
	}

	/**
	 * Throws an XMLValidationException to indicate that an element is
	 * unexpected.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param parentElementName
	 *            the name of the parent element
	 * @param unexpectedElementName
	 *            the name of the unexpected element
	 */
	static void errorUnexpectedElement(final String systemID, final int lineNr, final String parentElementName,
	                                   final String unexpectedElementName) throws XMLValidationException {
		throw new XMLValidationException(XMLValidationException.UNEXPECTED_ELEMENT, systemID, lineNr,
		    unexpectedElementName,
		    /* attributeName */null,
		    /* attributeValue */null, "Unexpected " + unexpectedElementName + " in a " + parentElementName);
	}

	/**
	 * Throws an XMLParseException to indicate that an entity reference is
	 * unexpected at this point.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param entity
	 *            the name of the entity
	 */
	static void errorUnexpectedEntity(final String systemID, final int lineNr, final String entity)
	        throws XMLParseException {
		throw new XMLParseException(systemID, lineNr, "No entity reference is expected here (" + entity + ")");
	}

	/**
	 * Throws an XMLValidationException to indicate that a #PCDATA element was
	 * unexpected.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param parentElementName
	 *            the name of the parent element
	 */
	static void errorUnexpectedPCData(final String systemID, final int lineNr, final String parentElementName)
	        throws XMLValidationException {
		throw new XMLValidationException(XMLValidationException.UNEXPECTED_PCDATA, systemID, lineNr,
		/* elementName */null,
		/* attributeName */null,
		/* attributeValue */null, "Unexpected #PCDATA in element " + parentElementName);
	}

	/**
	 * Throws an XMLParseException to indicate that the closing tag of an
	 * element does not match the opening tag.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param expectedName
	 *            the name of the opening tag
	 * @param wrongName
	 *            the name of the closing tag
	 */
	static void errorWrongClosingTag(final String systemID, final int lineNr, final String expectedName,
	                                 final String wrongName) throws XMLParseException {
		throw new XMLParseException(systemID, lineNr, "Closing tag does not match opening tag: `" + wrongName
		        + "' != `" + expectedName + "'");
	}

	/**
	 * Processes a character literal.
	 * 
	 * @param entity
	 *            the entity
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static char processCharLiteral(String entity) throws IOException, XMLParseException {
		if (entity.charAt(2) == 'x') {
			entity = entity.substring(3, entity.length() - 1);
			return (char) Integer.parseInt(entity, 16);
		}
		else {
			entity = entity.substring(2, entity.length() - 1);
			return (char) Integer.parseInt(entity, 10);
		}
	}

	/**
	 * Processes an entity.
	 * 
	 * @param entity
	 *            the entity
	 * @param reader
	 *            the reader
	 * @param entityResolver
	 *            the entity resolver
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static void processEntity(String entity, final IXMLReader reader, final IXMLEntityResolver entityResolver)
	        throws IOException, XMLParseException {
		entity = entity.substring(1, entity.length() - 1);
		final Reader entityReader = entityResolver.getEntity(reader, entity);
		if (entityReader == null) {
			XMLUtil.errorInvalidEntity(reader.getSystemID(), reader.getLineNr(), entity);
		}
		final boolean externalEntity = entityResolver.isExternalEntity(entity);
		reader.startNewStream(entityReader, !externalEntity);
	}

	/**
	 * Reads a character from the reader.
	 * 
	 * @param reader
	 *            the reader
	 * @param entityChar
	 *            the escape character (&amp; or %) used to indicate an entity
	 * @return the character, or an entity expression (like e.g. &amp;lt;)
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static String read(final IXMLReader reader, final char entityChar) throws IOException, XMLParseException {
		char ch = reader.read();
		final StringBuilder buf = new StringBuilder();
		buf.append(ch);
		if (ch == entityChar) {
			while (ch != ';') {
				ch = reader.read();
				buf.append(ch);
			}
		}
		return buf.toString();
	}

	/**
	 * Reads a character from the reader disallowing entities.
	 * 
	 * @param reader
	 *            the reader
	 * @param entityChar
	 *            the escape character (&amp; or %) used to indicate an entity
	 */
	static char readChar(final IXMLReader reader, final char entityChar) throws IOException, XMLParseException {
		final String str = XMLUtil.read(reader, entityChar);
		final char ch = str.charAt(0);
		if (ch == entityChar) {
			XMLUtil.errorUnexpectedEntity(reader.getSystemID(), reader.getLineNr(), str);
		}
		return ch;
	}

	/**
	 * Retrieves an identifier from the data.
	 * 
	 * @param reader
	 *            the reader
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static String scanIdentifier(final IXMLReader reader) throws IOException, XMLParseException {
		final StringBuilder result = new StringBuilder();
		for (;;) {
			final char ch = reader.read();
			if ((ch == '_') || (ch == ':') || (ch == '-') || (ch == '.') || ((ch >= 'a') && (ch <= 'z'))
			        || ((ch >= 'A') && (ch <= 'Z')) || ((ch >= '0') && (ch <= '9')) || (ch > '\u007E')) {
				result.append(ch);
			}
			else {
				reader.unread(ch);
				break;
			}
		}
		return result.toString();
	}

	/**
	 * Scans a public ID.
	 * 
	 * @param publicID
	 *            will contain the public ID
	 * @param reader
	 *            the reader
	 * @return the system ID
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static String scanPublicID(final StringBuilder publicID, final IXMLReader reader) throws IOException,
	        XMLParseException {
		if (!XMLUtil.checkLiteral(reader, "UBLIC")) {
			return null;
		}
		XMLUtil.skipWhitespace(reader, null);
		publicID.append(XMLUtil.scanString(reader, '\0', null));
		XMLUtil.skipWhitespace(reader, null);
		return XMLUtil.scanString(reader, '\0', null);
	}

	/**
	 * Retrieves a delimited string from the data.
	 * 
	 * @param reader
	 *            the reader
	 * @param entityChar
	 *            the escape character (&amp; or %)
	 * @param entityResolver
	 *            the entity resolver
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static String scanString(final IXMLReader reader, final char entityChar, final IXMLEntityResolver entityResolver)
	        throws IOException, XMLParseException {
		final StringBuilder result = new StringBuilder();
		final int startingLevel = reader.getStreamLevel();
		final char delim = reader.read();
		if ((delim != '\'') && (delim != '"')) {
			XMLUtil.errorExpectedInput(reader.getSystemID(), reader.getLineNr(), "delimited string");
		}
		for (;;) {
			String str = XMLUtil.read(reader, entityChar);
			final char ch = str.charAt(0);
			if (ch == entityChar) {
				if (str.charAt(1) == '#') {
					result.append(XMLUtil.processCharLiteral(str));
				}
				else {
					XMLUtil.processEntity(str, reader, entityResolver);
				}
			}
			else if (ch == '&') {
				reader.unread(ch);
				str = XMLUtil.read(reader, '&');
				if (str.charAt(1) == '#') {
					result.append(XMLUtil.processCharLiteral(str));
				}
				else {
					result.append(str);
				}
			}
			else if (reader.getStreamLevel() == startingLevel) {
				if (ch == delim) {
					break;
				}
				else if ((ch == 9) || (ch == 10) || (ch == 13)) {
					result.append(' ');
				}
				else {
					result.append(ch);
				}
			}
			else {
				result.append(ch);
			}
		}
		return result.toString();
	}

	/**
	 * Scans a system ID.
	 * 
	 * @param reader
	 *            the reader
	 * @return the system ID
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static String scanSystemID(final IXMLReader reader) throws IOException, XMLParseException {
		if (!XMLUtil.checkLiteral(reader, "YSTEM")) {
			return null;
		}
		XMLUtil.skipWhitespace(reader, null);
		return XMLUtil.scanString(reader, '\0', null);
	}

	/**
	 * Skips the remainder of a comment. It is assumed that &lt;!- is already
	 * read.
	 * 
	 * @param reader
	 *            the reader
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static void skipComment(final IXMLReader reader) throws IOException, XMLParseException {
		if (reader.read() != '-') {
			XMLUtil.errorExpectedInput(reader.getSystemID(), reader.getLineNr(), "<!--");
		}
		int dashesRead = 0;
		for (;;) {
			final char ch = reader.read();
			switch (ch) {
				case '-':
					dashesRead++;
					break;
				case '>':
					if (dashesRead == 2) {
						return;
					}
				default:
					dashesRead = 0;
			}
		}
	}

	/**
	 * Skips the remainder of the current XML tag.
	 * 
	 * @param reader
	 *            the reader
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static void skipTag(final IXMLReader reader) throws IOException, XMLParseException {
		int level = 1;
		while (level > 0) {
			final char ch = reader.read();
			switch (ch) {
				case '<':
					++level;
					break;
				case '>':
					--level;
					break;
			}
		}
	}

	/**
	 * Skips whitespace from the reader.
	 * 
	 * @param reader
	 *            the reader
	 * @param buffer
	 *            where to put the whitespace; null if the whitespace does not
	 *            have to be stored.
	 * @throws java.io.IOException
	 *             if an error occurred reading the data
	 */
	static void skipWhitespace(final IXMLReader reader, final StringBuilder buffer) throws IOException {
		char ch;
		if (buffer == null) {
			do {
				ch = reader.read();
			} while ((ch == ' ') || (ch == '\t') || (ch == '\n'));
		}
		else {
			for (;;) {
				ch = reader.read();
				if ((ch != ' ') && (ch != '\t') && (ch != '\n')) {
					break;
				}
				if (ch == '\n') {
					buffer.append('\n');
				}
				else {
					buffer.append(' ');
				}
			}
		}
		reader.unread(ch);
	}

	/**
	 * Throws an XMLValidationException.
	 * 
	 * @param systemID
	 *            the system ID of the data source
	 * @param lineNr
	 *            the line number in the data source
	 * @param message
	 *            the error message
	 * @param elementName
	 *            the name of the element
	 * @param attributeName
	 *            the name of the attribute
	 * @param attributeValue
	 *            the value of that attribute
	 */
	static void validationError(final String systemID, final int lineNr, final String message,
	                            final String elementName, final String attributeName, final String attributeValue)
	        throws XMLValidationException {
		throw new XMLValidationException(XMLValidationException.MISC_ERROR, systemID, lineNr, elementName,
		    attributeName, attributeValue, message);
	}
}
